|
1
|
|
|
/* |
|
2
|
|
|
* JavaScript Load Image Exif Parser |
|
3
|
|
|
* https://github.com/blueimp/JavaScript-Load-Image |
|
4
|
|
|
* |
|
5
|
|
|
* Copyright 2013, Sebastian Tschan |
|
6
|
|
|
* https://blueimp.net |
|
7
|
|
|
* |
|
8
|
|
|
* Licensed under the MIT license: |
|
9
|
|
|
* https://opensource.org/licenses/MIT |
|
10
|
|
|
*/ |
|
11
|
|
|
|
|
12
|
|
|
/* global define, Blob */ |
|
13
|
|
|
|
|
14
|
|
|
;(function (factory) { |
|
15
|
|
|
'use strict' |
|
16
|
|
|
if (typeof define === 'function' && define.amd) { |
|
17
|
|
|
// Register as an anonymous AMD module: |
|
18
|
|
|
define(['./load-image', './load-image-meta'], factory) |
|
19
|
|
|
} else if (typeof module === 'object' && module.exports) { |
|
20
|
|
|
factory(require('./load-image'), require('./load-image-meta')) |
|
21
|
|
|
} else { |
|
22
|
|
|
// Browser globals: |
|
23
|
|
|
factory(window.loadImage) |
|
24
|
|
|
} |
|
25
|
|
|
})(function (loadImage) { |
|
26
|
|
|
'use strict' |
|
27
|
|
|
|
|
28
|
|
|
loadImage.ExifMap = function () { |
|
29
|
|
|
return this |
|
30
|
|
|
} |
|
31
|
|
|
|
|
32
|
|
|
loadImage.ExifMap.prototype.map = { |
|
33
|
|
|
Orientation: 0x0112 |
|
34
|
|
|
} |
|
35
|
|
|
|
|
36
|
|
|
loadImage.ExifMap.prototype.get = function (id) { |
|
37
|
|
|
return this[id] || this[this.map[id]] |
|
38
|
|
|
} |
|
39
|
|
|
|
|
40
|
|
|
loadImage.getExifThumbnail = function (dataView, offset, length) { |
|
41
|
|
|
if (!length || offset + length > dataView.byteLength) { |
|
42
|
|
|
console.log('Invalid Exif data: Invalid thumbnail data.') |
|
|
|
|
|
|
43
|
|
|
return |
|
44
|
|
|
} |
|
45
|
|
|
return loadImage.createObjectURL( |
|
46
|
|
|
new Blob([dataView.buffer.slice(offset, offset + length)]) |
|
47
|
|
|
) |
|
48
|
|
|
} |
|
49
|
|
|
|
|
50
|
|
|
loadImage.exifTagTypes = { |
|
51
|
|
|
// byte, 8-bit unsigned int: |
|
52
|
|
|
1: { |
|
53
|
|
|
getValue: function (dataView, dataOffset) { |
|
54
|
|
|
return dataView.getUint8(dataOffset) |
|
55
|
|
|
}, |
|
56
|
|
|
size: 1 |
|
57
|
|
|
}, |
|
58
|
|
|
// ascii, 8-bit byte: |
|
59
|
|
|
2: { |
|
60
|
|
|
getValue: function (dataView, dataOffset) { |
|
61
|
|
|
return String.fromCharCode(dataView.getUint8(dataOffset)) |
|
62
|
|
|
}, |
|
63
|
|
|
size: 1, |
|
64
|
|
|
ascii: true |
|
65
|
|
|
}, |
|
66
|
|
|
// short, 16 bit int: |
|
67
|
|
|
3: { |
|
68
|
|
|
getValue: function (dataView, dataOffset, littleEndian) { |
|
69
|
|
|
return dataView.getUint16(dataOffset, littleEndian) |
|
70
|
|
|
}, |
|
71
|
|
|
size: 2 |
|
72
|
|
|
}, |
|
73
|
|
|
// long, 32 bit int: |
|
74
|
|
|
4: { |
|
75
|
|
|
getValue: function (dataView, dataOffset, littleEndian) { |
|
76
|
|
|
return dataView.getUint32(dataOffset, littleEndian) |
|
77
|
|
|
}, |
|
78
|
|
|
size: 4 |
|
79
|
|
|
}, |
|
80
|
|
|
// rational = two long values, first is numerator, second is denominator: |
|
81
|
|
|
5: { |
|
82
|
|
|
getValue: function (dataView, dataOffset, littleEndian) { |
|
83
|
|
|
return ( |
|
84
|
|
|
dataView.getUint32(dataOffset, littleEndian) / |
|
85
|
|
|
dataView.getUint32(dataOffset + 4, littleEndian) |
|
86
|
|
|
) |
|
87
|
|
|
}, |
|
88
|
|
|
size: 8 |
|
89
|
|
|
}, |
|
90
|
|
|
// slong, 32 bit signed int: |
|
91
|
|
|
9: { |
|
92
|
|
|
getValue: function (dataView, dataOffset, littleEndian) { |
|
93
|
|
|
return dataView.getInt32(dataOffset, littleEndian) |
|
94
|
|
|
}, |
|
95
|
|
|
size: 4 |
|
96
|
|
|
}, |
|
97
|
|
|
// srational, two slongs, first is numerator, second is denominator: |
|
98
|
|
|
10: { |
|
99
|
|
|
getValue: function (dataView, dataOffset, littleEndian) { |
|
100
|
|
|
return ( |
|
101
|
|
|
dataView.getInt32(dataOffset, littleEndian) / |
|
102
|
|
|
dataView.getInt32(dataOffset + 4, littleEndian) |
|
103
|
|
|
) |
|
104
|
|
|
}, |
|
105
|
|
|
size: 8 |
|
106
|
|
|
} |
|
107
|
|
|
} |
|
108
|
|
|
// undefined, 8-bit byte, value depending on field: |
|
109
|
|
|
loadImage.exifTagTypes[7] = loadImage.exifTagTypes[1] |
|
110
|
|
|
|
|
111
|
|
|
loadImage.getExifValue = function ( |
|
112
|
|
|
dataView, |
|
113
|
|
|
tiffOffset, |
|
114
|
|
|
offset, |
|
115
|
|
|
type, |
|
116
|
|
|
length, |
|
117
|
|
|
littleEndian |
|
118
|
|
|
) { |
|
119
|
|
|
var tagType = loadImage.exifTagTypes[type] |
|
120
|
|
|
var tagSize |
|
121
|
|
|
var dataOffset |
|
122
|
|
|
var values |
|
123
|
|
|
var i |
|
124
|
|
|
var str |
|
125
|
|
|
var c |
|
126
|
|
|
if (!tagType) { |
|
127
|
|
|
console.log('Invalid Exif data: Invalid tag type.') |
|
|
|
|
|
|
128
|
|
|
return |
|
129
|
|
|
} |
|
130
|
|
|
tagSize = tagType.size * length |
|
131
|
|
|
// Determine if the value is contained in the dataOffset bytes, |
|
132
|
|
|
// or if the value at the dataOffset is a pointer to the actual data: |
|
133
|
|
|
dataOffset = |
|
134
|
|
|
tagSize > 4 |
|
135
|
|
|
? tiffOffset + dataView.getUint32(offset + 8, littleEndian) |
|
136
|
|
|
: offset + 8 |
|
137
|
|
|
if (dataOffset + tagSize > dataView.byteLength) { |
|
138
|
|
|
console.log('Invalid Exif data: Invalid data offset.') |
|
139
|
|
|
return |
|
140
|
|
|
} |
|
141
|
|
|
if (length === 1) { |
|
142
|
|
|
return tagType.getValue(dataView, dataOffset, littleEndian) |
|
143
|
|
|
} |
|
144
|
|
|
values = [] |
|
145
|
|
|
for (i = 0; i < length; i += 1) { |
|
146
|
|
|
values[i] = tagType.getValue( |
|
147
|
|
|
dataView, |
|
148
|
|
|
dataOffset + i * tagType.size, |
|
149
|
|
|
littleEndian |
|
150
|
|
|
) |
|
151
|
|
|
} |
|
152
|
|
|
if (tagType.ascii) { |
|
153
|
|
|
str = '' |
|
154
|
|
|
// Concatenate the chars: |
|
155
|
|
|
for (i = 0; i < values.length; i += 1) { |
|
156
|
|
|
c = values[i] |
|
157
|
|
|
// Ignore the terminating NULL byte(s): |
|
158
|
|
|
if (c === '\u0000') { |
|
159
|
|
|
break |
|
160
|
|
|
} |
|
161
|
|
|
str += c |
|
162
|
|
|
} |
|
163
|
|
|
return str |
|
164
|
|
|
} |
|
165
|
|
|
return values |
|
166
|
|
|
} |
|
167
|
|
|
|
|
168
|
|
|
loadImage.parseExifTag = function ( |
|
169
|
|
|
dataView, |
|
170
|
|
|
tiffOffset, |
|
171
|
|
|
offset, |
|
172
|
|
|
littleEndian, |
|
173
|
|
|
data |
|
174
|
|
|
) { |
|
175
|
|
|
var tag = dataView.getUint16(offset, littleEndian) |
|
176
|
|
|
data.exif[tag] = loadImage.getExifValue( |
|
177
|
|
|
dataView, |
|
178
|
|
|
tiffOffset, |
|
179
|
|
|
offset, |
|
180
|
|
|
dataView.getUint16(offset + 2, littleEndian), // tag type |
|
181
|
|
|
dataView.getUint32(offset + 4, littleEndian), // tag length |
|
182
|
|
|
littleEndian |
|
183
|
|
|
) |
|
184
|
|
|
} |
|
185
|
|
|
|
|
186
|
|
|
loadImage.parseExifTags = function ( |
|
187
|
|
|
dataView, |
|
188
|
|
|
tiffOffset, |
|
189
|
|
|
dirOffset, |
|
190
|
|
|
littleEndian, |
|
191
|
|
|
data |
|
192
|
|
|
) { |
|
193
|
|
|
var tagsNumber, dirEndOffset, i |
|
194
|
|
|
if (dirOffset + 6 > dataView.byteLength) { |
|
195
|
|
|
console.log('Invalid Exif data: Invalid directory offset.') |
|
|
|
|
|
|
196
|
|
|
return |
|
197
|
|
|
} |
|
198
|
|
|
tagsNumber = dataView.getUint16(dirOffset, littleEndian) |
|
199
|
|
|
dirEndOffset = dirOffset + 2 + 12 * tagsNumber |
|
200
|
|
|
if (dirEndOffset + 4 > dataView.byteLength) { |
|
201
|
|
|
console.log('Invalid Exif data: Invalid directory size.') |
|
202
|
|
|
return |
|
203
|
|
|
} |
|
204
|
|
|
for (i = 0; i < tagsNumber; i += 1) { |
|
205
|
|
|
this.parseExifTag( |
|
206
|
|
|
dataView, |
|
207
|
|
|
tiffOffset, |
|
208
|
|
|
dirOffset + 2 + 12 * i, // tag offset |
|
209
|
|
|
littleEndian, |
|
210
|
|
|
data |
|
211
|
|
|
) |
|
212
|
|
|
} |
|
213
|
|
|
// Return the offset to the next directory: |
|
214
|
|
|
return dataView.getUint32(dirEndOffset, littleEndian) |
|
215
|
|
|
} |
|
216
|
|
|
|
|
217
|
|
|
loadImage.parseExifData = function (dataView, offset, length, data, options) { |
|
218
|
|
|
if (options.disableExif) { |
|
219
|
|
|
return |
|
220
|
|
|
} |
|
221
|
|
|
var tiffOffset = offset + 10 |
|
222
|
|
|
var littleEndian |
|
223
|
|
|
var dirOffset |
|
224
|
|
|
var thumbnailData |
|
225
|
|
|
// Check for the ASCII code for "Exif" (0x45786966): |
|
226
|
|
|
if (dataView.getUint32(offset + 4) !== 0x45786966) { |
|
227
|
|
|
// No Exif data, might be XMP data instead |
|
228
|
|
|
return |
|
229
|
|
|
} |
|
230
|
|
|
if (tiffOffset + 8 > dataView.byteLength) { |
|
231
|
|
|
console.log('Invalid Exif data: Invalid segment size.') |
|
|
|
|
|
|
232
|
|
|
return |
|
233
|
|
|
} |
|
234
|
|
|
// Check for the two null bytes: |
|
235
|
|
|
if (dataView.getUint16(offset + 8) !== 0x0000) { |
|
236
|
|
|
console.log('Invalid Exif data: Missing byte alignment offset.') |
|
237
|
|
|
return |
|
238
|
|
|
} |
|
239
|
|
|
// Check the byte alignment: |
|
240
|
|
|
switch (dataView.getUint16(tiffOffset)) { |
|
241
|
|
|
case 0x4949: |
|
242
|
|
|
littleEndian = true |
|
243
|
|
|
break |
|
244
|
|
|
case 0x4d4d: |
|
245
|
|
|
littleEndian = false |
|
246
|
|
|
break |
|
247
|
|
|
default: |
|
248
|
|
|
console.log('Invalid Exif data: Invalid byte alignment marker.') |
|
249
|
|
|
return |
|
250
|
|
|
} |
|
251
|
|
|
// Check for the TIFF tag marker (0x002A): |
|
252
|
|
|
if (dataView.getUint16(tiffOffset + 2, littleEndian) !== 0x002a) { |
|
253
|
|
|
console.log('Invalid Exif data: Missing TIFF marker.') |
|
254
|
|
|
return |
|
255
|
|
|
} |
|
256
|
|
|
// Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: |
|
257
|
|
|
dirOffset = dataView.getUint32(tiffOffset + 4, littleEndian) |
|
258
|
|
|
// Create the exif object to store the tags: |
|
259
|
|
|
data.exif = new loadImage.ExifMap() |
|
260
|
|
|
// Parse the tags of the main image directory and retrieve the |
|
261
|
|
|
// offset to the next directory, usually the thumbnail directory: |
|
262
|
|
|
dirOffset = loadImage.parseExifTags( |
|
263
|
|
|
dataView, |
|
264
|
|
|
tiffOffset, |
|
265
|
|
|
tiffOffset + dirOffset, |
|
266
|
|
|
littleEndian, |
|
267
|
|
|
data |
|
268
|
|
|
) |
|
269
|
|
|
if (dirOffset && !options.disableExifThumbnail) { |
|
270
|
|
|
thumbnailData = { exif: {} } |
|
271
|
|
|
dirOffset = loadImage.parseExifTags( |
|
|
|
|
|
|
272
|
|
|
dataView, |
|
273
|
|
|
tiffOffset, |
|
274
|
|
|
tiffOffset + dirOffset, |
|
275
|
|
|
littleEndian, |
|
276
|
|
|
thumbnailData |
|
277
|
|
|
) |
|
278
|
|
|
// Check for JPEG Thumbnail offset: |
|
279
|
|
|
if (thumbnailData.exif[0x0201]) { |
|
280
|
|
|
data.exif.Thumbnail = loadImage.getExifThumbnail( |
|
281
|
|
|
dataView, |
|
282
|
|
|
tiffOffset + thumbnailData.exif[0x0201], |
|
283
|
|
|
thumbnailData.exif[0x0202] // Thumbnail data length |
|
284
|
|
|
) |
|
285
|
|
|
} |
|
286
|
|
|
} |
|
287
|
|
|
// Check for Exif Sub IFD Pointer: |
|
288
|
|
|
if (data.exif[0x8769] && !options.disableExifSub) { |
|
289
|
|
|
loadImage.parseExifTags( |
|
290
|
|
|
dataView, |
|
291
|
|
|
tiffOffset, |
|
292
|
|
|
tiffOffset + data.exif[0x8769], // directory offset |
|
293
|
|
|
littleEndian, |
|
294
|
|
|
data |
|
295
|
|
|
) |
|
296
|
|
|
} |
|
297
|
|
|
// Check for GPS Info IFD Pointer: |
|
298
|
|
|
if (data.exif[0x8825] && !options.disableExifGps) { |
|
299
|
|
|
loadImage.parseExifTags( |
|
300
|
|
|
dataView, |
|
301
|
|
|
tiffOffset, |
|
302
|
|
|
tiffOffset + data.exif[0x8825], // directory offset |
|
303
|
|
|
littleEndian, |
|
304
|
|
|
data |
|
305
|
|
|
) |
|
306
|
|
|
} |
|
307
|
|
|
} |
|
308
|
|
|
|
|
309
|
|
|
// Registers the Exif parser for the APP1 JPEG meta data segment: |
|
310
|
|
|
loadImage.metaDataParsers.jpeg[0xffe1].push(loadImage.parseExifData) |
|
311
|
|
|
|
|
312
|
|
|
// Adds the following properties to the parseMetaData callback data: |
|
313
|
|
|
// * exif: The exif tags, parsed by the parseExifData method |
|
314
|
|
|
|
|
315
|
|
|
// Adds the following options to the parseMetaData method: |
|
316
|
|
|
// * disableExif: Disables Exif parsing. |
|
317
|
|
|
// * disableExifThumbnail: Disables parsing of the Exif Thumbnail. |
|
318
|
|
|
// * disableExifSub: Disables parsing of the Exif Sub IFD. |
|
319
|
|
|
// * disableExifGps: Disables parsing of the Exif GPS Info IFD. |
|
320
|
|
|
}) |
|
321
|
|
|
|